﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace Babel.SDK
{
    public sealed class MapFile
    {
        #region Fields
        private XDocument _document;
        private Dictionary<string, TargetAssemblyElement> _dictionary;
        private TypeSignatureEqualityComparer _eq;
        #endregion

        #region Events
        public event EventHandler<UnresolvedStackFrameEventArgs> UnresolvedStackFrame;
        public event EventHandler<MultipleStackFrameMatchEventArgs> MultipleStackFrameMatch;
        
        #endregion

        #region Properties
        public static Stream BabelMapFileSchemaStream
        {
            get
            {
                return typeof(MapFile).Assembly.GetManifestResourceStream("Babel.SDK.babel.map.xsd");
            }
        }

        public XDocument Document
        {
            get { return _document; }
        }
        #endregion

        #region Constructors
        internal MapFile(string fileName)
        {
            _dictionary = new Dictionary<string, TargetAssemblyElement>();
            _eq = new TypeSignatureEqualityComparer();
            _eq.SkipNamespaceCheck = true;
            LoadMap(fileName);
        }
        
        #endregion

        #region Methods
        public static MapFile Load(string fileName)
        {
            MapFile map = new MapFile(fileName);
            return map;
        }

        public string DeobfuscateStack(TextReader stream)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");

            StringBuilder deStack = new StringBuilder();
            int lineNumber = 1;
            while (stream.Peek() != -1)
            {
                string ln = stream.ReadLine();
                deStack.AppendLine(DeobfuscateStackFrame(ln, lineNumber));
                lineNumber++;
            }

            return deStack.ToString();
        }

        public string DeobfuscateStackFrame(string ln, int lineNumber)
        {
            StackFrameString stk;
            if (StackFrameString.TryParse(ln, out stk))
            {
                List<string> decoded;
				XElement typeNode = Resolve(stk, out decoded);
                if (typeNode != null)
                {
					if (decoded.Count == 0)
					{
						string stackFrame = stk.Frame.Replace(stk.FullTypeName, BuildFullTypeName(typeNode));
						OnUnresolvedStackFrame(new UnresolvedStackFrameEventArgs(stackFrame, lineNumber));					
						return ln.Replace(stk.Frame, stackFrame);
					}
					else if (decoded.Count >= 1)
					{
						if (decoded.Count > 1)
							OnMultipleStackFrameMatch(new MultipleStackFrameMatchEventArgs(stk.Frame, lineNumber, decoded));

						return ln.Replace(stk.Frame, decoded[0]);
					}
                }
            }
            return ln;
        }

		private XElement Resolve(StackFrameString stk, out List<string> possibleMatch)
        {
			possibleMatch = new List<string>();
            List<XElement> typeNodeList = FindTypeNode(stk.FullTypeName);
			foreach (XElement typeNode in typeNodeList)
			{
                List<XElement> methodNodeList = FindMethodNodes(typeNode, stk.MethodName, stk.Arguments);

                if (methodNodeList.Count > 0)
                {
                    foreach (XElement metnodNode in methodNodeList)
                    {
                        possibleMatch.Add(BuildStackTraceLine(typeNode, metnodNode));
                    }
                }
				return typeNode;
            }
            return null;
        }

		private string BuildFullTypeName(XElement typeNode)
		{
			string fullTypeName = String.Empty;
			XAttribute attrName = typeNode.Attribute("name");
			if (attrName != null)
			{
				fullTypeName = attrName.Value;
				XElement parent = typeNode.Parent;
				while (parent != null)
				{
					if (parent.Name.LocalName.Equals("TargetAssembly", StringComparison.Ordinal))
						break;

					fullTypeName = parent.Attribute("name").Value + "." + fullTypeName;
					parent = parent.Parent;
				}
			}

			return fullTypeName;
		}

        private string BuildStackTraceLine(XElement typeNode, XElement methodNode)
        {
			string fullTypeName = BuildFullTypeName(typeNode);

            // Build parameter list
            StringBuilder sb = new StringBuilder();
            sb.Append("(");
            XElement parameterList = methodNode.Element("Parameters");
            if (parameterList != null)
            {
                XElement[] parameters = parameterList.Elements("Parameter").ToArray();
                for (int i = 0; i < parameters.Length; i++)
                {
                    if (i > 0)
                        sb.Append(" ,");

                    XElement par = parameters[i];
                    sb.AppendFormat("{0} {1}", par.Attribute("type").Value, par.Attribute("name").Value);
                }
            }
            sb.Append(")");
            return fullTypeName + "." + methodNode.Attribute("name").Value + sb.ToString();
        }

        private List<XElement> FindTypeNode(string fullName)
        {
			// Try first full name
			List<XElement> elementTypes = SelectTypes(_document.Root, "newFullName", fullName);

			int pos = fullName.LastIndexOf('.');
			string cl = String.Empty;
			while (pos != -1)
			{
				string ns = fullName.Substring(0, pos);
				cl = ns + fullName.Substring(pos).Replace(".", "/");

				// Check nested types
				List<XElement> nestedTypes = SelectTypes(_document.Root, "newFullName", cl);
				elementTypes.AddRange(nestedTypes);

				fullName = cl;
				pos = fullName.LastIndexOf('.');
			}

			return elementTypes;
        }

        private List<XElement> FindMethodNodes(XElement typeNode, string name, List<string> args)
        {
            if (typeNode == null)
                throw new ArgumentNullException("typeNode");

            if (name == null)
                throw new ArgumentNullException("name");

            if (args == null)
                throw new ArgumentNullException("args");

            List<XElement> methods = new List<XElement>();
			List<XElement> methodElements = SelectMethods(typeNode, "newName", name);
            if (methodElements.Count == 0)
            {
				methodElements = SelectMethods(typeNode, "name", name);
            }

            var query = from method in methodElements
                        let parameters = method.Element("Parameters")
						where ((args.Count == 0) && (parameters == null)) || 
						((parameters != null) && (parameters.Elements("Parameter").Count() == args.Count))
                        select method;

            foreach (XElement node in query)
            {
                int i = 0;
                int paramCount = args.Count;
                if (paramCount > 0)
                {
                    // Parameters check
                    XElement[] parameters = node.Element("Parameters").Elements("Parameter").ToArray();
                    for (; i < paramCount; i++)
                    {
						// break loop on each error 
						// continue if OK

						string typePar = (string)parameters[i].Attribute("type");
						if (typePar == null)
							break;

						string[] stackArg = args[i].Split(' ');
						if (stackArg.Length != 2)
							break;

						// First is the type name
						string stackType = stackArg[0];
						if (_eq.Equals(typePar, stackType))
							continue;

						List<XElement> nodeTypeList = SelectTypes(_document.Root, "newName", stackType);
						if (MatchAnyTypeName(nodeTypeList, typePar))
							continue;

						// Not found. This method does not match.
						break;
                    }

                    // Parameter does not match
                    if (i != paramCount)
                        continue;
                }

                // Constructor not need return type check
                // if ((name == ".ctor" || name == ".cctor"))

                methods.Add(node);
            }
            return methods;
        }

		private List<XElement> SelectMethods(XElement typeNode, string attribute, string value)
		{
			string xPathMethods = "(.//Method|.//Set|.//Get|.//Add|.//Remove|.//Invoke)";
			string xpath = String.Format("{0}[@{1}='{2}']", xPathMethods, attribute, value);
			return typeNode.XPathSelectElements(xpath).ToList();
		}

		private List<XElement> SelectTypes(XElement typeNode, string attribute, string value)
		{
			string xPathMethods = "(.//Class|.//Structure|.//Interface|.//Enum)";
			string xpath = String.Format("{0}[@{1}='{2}']", xPathMethods, attribute, value);
			return typeNode.XPathSelectElements(xpath).ToList();
		}

		private bool MatchAnyTypeName(List<XElement> nodeTypeList, string typeName)
		{
			foreach (XElement nodeType in nodeTypeList)
			{
				if (_eq.Equals(typeName, BuildFullTypeName(nodeType)))
					return true;
			}
			return false;
		}

        private void OnUnresolvedStackFrame(UnresolvedStackFrameEventArgs e)
        {
            EventHandler<UnresolvedStackFrameEventArgs> handler = this.UnresolvedStackFrame;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnMultipleStackFrameMatch(MultipleStackFrameMatchEventArgs e)
        {
            EventHandler<MultipleStackFrameMatchEventArgs> handler = this.MultipleStackFrameMatch;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        private MethodMap FindeMethodMap(TypeMap tm, string name, List<string> args)
        {
            // Select 
            var query = from m in tm.Items.OfType<MethodMap>()
                        where m.newName == name && m.Parameters.Length == args.Count
                        select m;

            foreach (MethodMap m in query)
            {
                int i = 0;
                for (; i < args.Count; i++)
                {
                    ParameterMap pm = m.Parameters[i];

                    string arg = args[i];
                    string[] item = arg.Split(' ');
                    if (!pm.type.Equals(item[0], StringComparison.Ordinal) ||
                        !pm.name.Equals(item[1], StringComparison.Ordinal))
                        break;
                }

                // Method found
                if (i == args.Count)
                    return m;
            }

            return null;
        }

        private static BabelMap Create(string filePath)
        {
            if (filePath == null)
                throw new ArgumentNullException("filePath");

            // Create an instance of the XmlSerializer class;
            // specify the type of object to de-serialize.
            XmlSerializer serializer = new XmlSerializer(typeof(BabelMap));

            // If the XML document has been altered with unknown 
            // nodes or attributes, handle them with the 
            // UnknownNode and UnknownAttribute events.
            serializer.UnknownNode += new XmlNodeEventHandler(BabelMap_UnknownNode);
            serializer.UnknownAttribute += new XmlAttributeEventHandler(BabelMap_UnknownAttribute);

            // Get embedded resource schema
            XmlSchemaSet xmlSchemas = new XmlSchemaSet();
            xmlSchemas.Add(null, XmlReader.Create(BabelMapFileSchemaStream));

            // An XmlReader is needed to read the XML document
            FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            XmlTextReader xmlRd = new XmlTextReader(fs);

            XmlReaderSettings settings = new XmlReaderSettings();
            settings.ValidationType = ValidationType.Schema;            
            settings.Schemas = xmlSchemas;

            XmlReader xmlReader = XmlReader.Create(xmlRd, settings);

            // Use the Deserialize method to restore the object's state with
            // data from the XML document
            BabelMap babelMap = (BabelMap)serializer.Deserialize(xmlReader);

            return babelMap;
        }

        private static void BabelMap_UnknownAttribute(object sender, XmlAttributeEventArgs e)
        {
            Console.WriteLine("Unknown attribute {0}=\"{1}\" at ({2}, {3})",
                e.Attr.Name, e.Attr.Value, e.LineNumber, e.LinePosition);
        }

        private static void BabelMap_UnknownNode(object sender, XmlNodeEventArgs e)
        {
            Console.WriteLine("Unknown node {0} type {1} at ({2}, {3})",
                e.Name, e.NodeType, e.LineNumber, e.LinePosition);
        }

        private void LoadMap(string fileName)
        {
            if (String.IsNullOrEmpty(fileName))
                throw new ArithmeticException("fileName");

            FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
            // Get embedded resource schema
            XmlSchemaSet xmlSchemas = new XmlSchemaSet();
            xmlSchemas.Add(null, XmlReader.Create(BabelMapFileSchemaStream));

            XmlReaderSettings settings = new XmlReaderSettings();
            settings.ConformanceLevel = ConformanceLevel.Document;
            settings.ValidationType = ValidationType.Schema;
            settings.ValidationEventHandler += new ValidationEventHandler(settings_ValidationEventHandler);
            settings.Schemas = xmlSchemas;

            using (var rdr = XmlReader.Create(fs, settings))
            {
                _document = XDocument.Load(rdr, LoadOptions.None);
            }
        }

        private static void settings_ValidationEventHandler(object sender, ValidationEventArgs e)
        {
            XmlSchemaValidationException ex = e.Exception as XmlSchemaValidationException;
            string msg = String.Format("{0}: {1}", e.Severity, e.Message);

            if (ex != null)
                msg += String.Format(" at ({0}, {1})", ex.LineNumber, ex.LinePosition);
            
            throw new InvalidOperationException(msg, ex);
        }

        private static IEnumerable<TargetAssemblyElement> StreamTargetAssemblies(string file)
        {
            using (var rdr = XmlReader.Create(file))
            {
                rdr.MoveToContent();
                while (rdr.Read())
                {
                    if ((rdr.NodeType == XmlNodeType.Element) && (rdr.Name == TargetAssemblyElement.NodeTargetAssembly))
                    {
                        var e = new TargetAssemblyElement((XElement)XElement.ReadFrom(rdr));
                        yield return e;
                    }
                }
                rdr.Close();
            }
        }

        private void InferSchema(string fileName)
        {
            XmlSchemaInference inference = new XmlSchemaInference();
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(_document.ToString()));
            XmlReader reader = XmlReader.Create(ms);
            XmlSchemaSet schemaSet = inference.InferSchema(reader);
            using (FileStream xsd = File.Open(fileName, FileMode.Create))
            {
                foreach (XmlSchema s in schemaSet.Schemas())
                {
                    s.Write(xsd);
                }
            }
        }
        
        #endregion
    }
}
